package idv.cjcat.stardust.common.xml;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Comparator;
import java.util.Arrays;

import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import idv.cjcat.stardust.common.errors.DuplicateElementNameError;
import idv.cjcat.stardust.common.StardustElement;
import idv.cjcat.stardust.Stardust;

public class XMLBuilder {
	
	//XML building
	//------------------------------------------------------------------------------------------------
	
	/**
	 * Generate the XML representation of an Stardust element.
	 * 
	 * <p>
	 * All related elements' would be included in the XML representation.
	 * </p>
	 * @param	element
	 * @return
	 */
	public static Element buildXML(StardustElement element) {
		Element root = XMLElement.document.createElement("StardustParticleSystem");
		root.setAttribute("version", Stardust.VERSION);
            
		Hashtable<String,StardustElement> relatedElements = new Hashtable<String,StardustElement>();
		traverseRelatedObjects(element, relatedElements);
            
		Comparator<StardustElement> elementTypeSorter = new ElementTypeSorter();
            
		StardustElement[] relatedElementsArray = new StardustElement[relatedElements.size()];
		Enumeration<StardustElement> elements = relatedElements.elements();
		int i=0;
		while(elements.hasMoreElements()){
			relatedElementsArray[i]=elements.nextElement();
			i++;
		}
		Arrays.sort(relatedElementsArray, elementTypeSorter);
			
		for (StardustElement e : relatedElementsArray) {
			Element elementXML = e.toXML();
			Element typeXML = e.getElementTypeXMLTag();
				
			if (root.getElementsByTagName(typeXML.getTagName()).getLength() == 0) {
				root.appendChild(typeXML);
			}
			root.getElementsByTagName(typeXML.getTagName()).item(0).appendChild(elementXML);
		}
		
		return root;
	}
	
	private static void traverseRelatedObjects(StardustElement element, Hashtable<String,StardustElement> relatedElements) {
		if (element==null) return;
		
		if (relatedElements.containsKey(element.name)) {
			if (relatedElements.get(element.name) != element) {
				throw new DuplicateElementNameError("Duplicate element name: " + element.name, element.name, relatedElements.get(element.name), element);
			}
		} else {
			relatedElements.put(element.name, element);
		}
		for (StardustElement e : element.getRelatedObjects()) {
			traverseRelatedObjects(e, relatedElements);
		}
	}
	
	//------------------------------------------------------------------------------------------------
	//end of XML building
	
	private Hashtable<String,Class<StardustElement>> elementClasses;
	private Hashtable<String,StardustElement> elements;
	
	public XMLBuilder() {
		elementClasses = new Hashtable<String,Class<StardustElement>>();
		elements = new Hashtable<String,StardustElement>();
		
		//registerClassesFromClassPackage(CommonClassPackage.getInstance());
		//registerClassesFromClassPackage(TwoDClassPackage.getInstance());
		//registerClassesFromClassPackage(ThreeDClassPackage.getInstance());
	}
	
	/**
	 * To use <code>XMLBuilder</code> with your custom subclasses of Stardust elements, 
	 * you must register your class and XML tag name first.
	 * 
	 * <p>
	 * For example, if you register the <code>MyAction</code> class with XML tag name "HelloWorld", 
	 * <code>XMLBuilder</code> knows you are refering to the <code>MyAction</code> class when a <HelloWorld> tag appears in the XML representation.
	 * All default classes in the Stardust engine are already registered, 
	 * </p>
	 * @param	elementClass
	 * @param	xmlTagClassName
	 * @throws IllegalAccessException 
	 * @throws InstantiationException 
	 */
	public void registerClass(Class<StardustElement> elementClass) throws InstantiationException, IllegalAccessException {
		StardustElement element = elementClass.newInstance();
		if (element.getClass().equals(StardustElement.class)) {
			throw new Error("The class is not a subclass of the StardustElement class.");
		}
		if (elementClasses.containsKey(element.getXMLTagName())) {
			throw new Error("This element class name is already registered: " + element.getXMLTagName());
		}
		elementClasses.put(element.getXMLTagName(), elementClass);
	}
	
	/**
	 * Registers multiple classes.
	 * @param	classes
	 * @throws IllegalAccessException 
	 * @throws InstantiationException 
	 */
	public void registerClasses(Class<StardustElement>[] classes) throws InstantiationException, IllegalAccessException {
		for (Class<StardustElement> c : classes) {
			registerClass(c);
		}
	}
	
	/**
	 * Registers multiple classes from a <code>ClassPackage</code> object.
	 * @param	classPackage
	 */
	/*TODO:public void registerClassesFromClassPackage(ClassPackage classPackage) {
		registerClasses(classPackage.getClasses());
	}*/
	
	/**
	 * Undos the XML tag name registration.
	 * @param	name
	 */
	public void unregisterClass(String name) {
		elementClasses.remove(name);
	}
	
	/**
	 * After reconstructing elements through the <code>buildFromXML()</code> method, 
	 * reconstructed elements can be extracted through this method.
	 * 
	 * <p>
	 * Each Stardust element has a name; this name is used to identify elements.
	 * </p>
	 * @param	name
	 * @return
	 */
	public StardustElement getElementByName(String name) {
		if (!elements.contains(name)) {
			throw new Error("Element not found: " + name);
		}
		return elements.get(name);
	}
	
	/**
	 * Reconstructs elements from XML representations.
	 * 
	 * <p>
	 * After calling this method, you may extract constrcuted elements through the <code>getElementByName()</code> method.
	 * </p>
	 * @param	xml
	 * @throws IllegalAccessException 
	 * @throws InstantiationException 
	 */
	public void buildFromXML(Element xml) throws InstantiationException, IllegalAccessException {
		if (!elements.isEmpty()) {
			//TODO
			elements.clear();
		}
		elements = new Hashtable<String,StardustElement>();
		
		StardustElement element;
		NodeList types = xml.getChildNodes();
		for(int i=0; i<types.getLength(); i++){
			NodeList el = types.item(i).getChildNodes();
			for(int j=0; i<el.getLength(); j++){
				Element node = (Element) el.item(j);
				element = elementClasses.get(node.getAttribute("name")).newInstance();
				if(elements.containsKey(node.getAttribute("name"))){
					throw new DuplicateElementNameError("Duplicate element name: " + node.getAttribute("name"), node.getAttribute("name"), elements.get(node.getAttribute("name")), element);
				}
				elements.put(node.getAttribute("name"), element);
				element.parseXML(node, this);
			}
		}
	}
}

//-----End of XMLBuilder Class----

class ElementTypeSorter implements Comparator<StardustElement> {
	public int compare(StardustElement e1, StardustElement e2) {
		if (e1.getElementTypeXMLTag().getTagName().compareToIgnoreCase(e2.getElementTypeXMLTag().getTagName()) != 0) 
			return e1.getElementTypeXMLTag().getTagName().compareToIgnoreCase(e2.getElementTypeXMLTag().getTagName());
		
		if (e1.getXMLTagName().compareToIgnoreCase(e2.getXMLTagName()) != 0) 
			return e1.getXMLTagName().compareToIgnoreCase(e2.getXMLTagName());
		
		return e1.name.compareToIgnoreCase(e2.name);
	}
}